package com.libre.security;

import com.alibaba.fastjson.JSON;
import com.libre.auth.entity.User;
import com.libre.auth.service.LoginLogService;
import com.libre.common.constant.SysConstant;
import com.libre.common.enums.ResultEnum;
import com.libre.common.utils.ResponseUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

// 1. Security 配置
@Slf4j
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)//开启基于方法的安全认证机制，也就是说在web层的controller启用注解机制的安全确认
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //JWT签名加密key
    @Value("${auth.jwt-key}")
    private String jwtSignKey;

    //超时时长
    @Value("${auth.expire-time}")
    private Integer expireTime = 30;

    @Autowired
    private StringRedisTemplate redisTemplate;

    //自定义验证（密码）
    @Autowired
    private MyAuthenticationProvider authenticationProvider;

    private UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;

    private UrlAccessDecisionManager urlAccessDecisionManager;

    //使用jwt验证（token）
    @Bean
    public JWTAuthenticationFilter authenticationTokenFilter() {
        return new JWTAuthenticationFilter(jwtSignKey);
    }

    public WebSecurityConfig(UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource, UrlAccessDecisionManager urlAccessDecisionManager) {
        this.urlFilterInvocationSecurityMetadataSource = urlFilterInvocationSecurityMetadataSource;
        this.urlAccessDecisionManager = urlAccessDecisionManager;
    }


    @Autowired
    LoginLogService loginLogService;

    // 静态资源  todo 找不到原因
//    @Override
//    public void configure(WebSecurity web) throws Exception {
//        System.out.println(">>>>>>>>>>>>>>>> WebSecurity >>>>>>>>>>>>>>>>");
//        web.ignoring().antMatchers("/static/resource/**", "/favicon.ico", "*.jpg");//理论上绕过springsecurity所有filter
//    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        // 开启允许iframe 嵌套
        registry.and().headers().frameOptions().disable();
        /***
         *  permitAll()代表任意用户可访问
         *  anyRequest() 表示所有请求
         *  authenticated() 表示已登录用户才能访问
         *  accessDecisionManager() 表示绑定在 url 上的鉴权管理器
         *  addFilterBefore 加在对应的过滤器之前
         *  addFilterAfter 加在对应的过滤器之后
         *  LogoutSuccessHandler 登出成功处理器
         *  继承 SimpleUrlAuthenticationFailureHandler 或者实现 AuthenticationFailureHandler，建议采用继承。登录失败处理器
         * */
        registry.and()
                .csrf() //跨站
                .disable() //关闭跨站检CreateVerifyCode测
                .cors() // 开启跨域

                // 其余所有请求都需要认证
                .and()
                .authorizeRequests()

                // 允许
                // .antMatchers("/index/index").permitAll()

                .anyRequest()
                .authenticated()

                .and()
                .formLogin()
                //这里配置的loginProcessingUrl为页面中对应表单的 action ，该请求为 post，并设置可匿名访问
                .loginProcessingUrl("/login").permitAll()
                //登录成功后的返回结果
                .successHandler(new AuthenticationSuccessHandlerImpl())
                //登录失败后的返回结果
                .failureHandler(new AuthenticationFailureHandlerImpl())
                //这里配置的logoutUrl为登出接口，并设置可匿名访问
                .and().logout()
                // .permitAll()
                //登出后的返回结果
                .logoutSuccessHandler(new LogoutSuccessHandlerImpl())

                //这里配置的为当未登录访问受保护资源时，返回json，并且让springsecurity自带的登录界面失效
                .and().exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPointHandler())
                .accessDeniedHandler(new CustomAccessDeineHandler())
                .and()
                /**
                 * session策略，此处选用禁用session
                 *  ALWAYS:总是创建HttpSession
                 *  IF_REQUIRED:Spring Security只会在需要时创建一个HttpSession
                 *  NEVER:Spring Security不会创建HttpSession，但如果它已经存在，将可以使用HttpSession
                 *  STATELESS:Spring Security永远不会创建HttpSession，它不会使用HttpSession来获取SecurityContext
                 */
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 添加JWT filter
        registry.and().addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        // url权限认证处理
        registry.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                o.setAccessDecisionManager(urlAccessDecisionManager);
                return o;
            }
        });
    }


    //授权
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //将验证过程交给自定义验证工具
        auth.authenticationProvider(authenticationProvider);
    }

    //定义登陆成功返回信息
    private class AuthenticationSuccessHandlerImpl extends SimpleUrlAuthenticationSuccessHandler {
        /*
           request：相当与HttpServletRequest
           response：相当与HttpServletRespose
           authentication：这里保存了我们登录后的用户信息
         */
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            User user = (User) authentication.getPrincipal();
            String username = ((UserDetails) authentication.getPrincipal()).getUsername();
            log.info(">>>>>>>>> 用户{}登陆成功 >>>>>>>>>", username);
            String userId = ((User) authentication.getPrincipal()).getId();
            List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails) authentication.getPrincipal()).getAuthorities();
            List<String> list = new ArrayList<>();
            for (GrantedAuthority g : authorities) {
                list.add(g.getAuthority());
            }

            //登陆成功，将用户角色保存到reids中,30分钟
            /**
             *   TimeUnit.DAYS          //天
             *   TimeUnit.HOURS         //小时
             *   TimeUnit.MINUTES       //分钟
             *   TimeUnit.SECONDS       //秒
             *   TimeUnit.MILLISECONDS  //毫秒
             */
            // 通过JWT生成token
            String token = SysConstant.TOKEN_SPLIT + Jwts.builder()
                    //自定义属性 放入用户拥有请求权限
                    .claim(SysConstant.AUTHORITIES, JSON.toJSON(list).toString())
                    //主题 放入用户名
                    .setSubject(username)
                    //失效时间
                    .setExpiration(new Date(System.currentTimeMillis() + expireTime * 60 * 1000))
                    //签名算法和密钥
                    .signWith(SignatureAlgorithm.HS512, jwtSignKey)
                    .compact();
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, authorities);
            // 全局注入角色权限信息和登录用户基本信息
            SecurityContextHolder.getContext().setAuthentication(auth);
            //token存入redis
            redisTemplate.opsForValue().set(SysConstant.REDIS_ACCESS_TOKEN + username, token, expireTime, TimeUnit.MINUTES);
            // 角色信息
            redisTemplate.opsForValue().set(SysConstant.REDIS_ROLE + username, JSON.toJSON(authorities).toString(), expireTime, TimeUnit.MINUTES);

            //登录日志
            loginLogService.saveLoginLog(request, username);

            Map map = new HashMap<>();
            map.put(SysConstant.TOKEN_NAME, token);
            map.put("user", user);
            new ResponseUtil(response, ResultEnum.LOG_IN_SUCCESSFULLY.getCode(), true, ResultEnum.LOG_IN_SUCCESSFULLY.getReasonPhraseCN(), map);//登录成功
        }
    }

    //定义登出成功返回信息
    private class LogoutSuccessHandlerImpl extends SimpleUrlLogoutSuccessHandler {
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws IOException, ServletException {
            // 进行token校验
            UsernamePasswordAuthenticationToken auth = authenticationTokenFilter().getAuthentication(request, response);
            String username = auth.getName();
            // 清理redis中 token
            redisTemplate.delete(SysConstant.REDIS_ACCESS_TOKEN + username);
            // 清理redis中 角色
            redisTemplate.delete(SysConstant.REDIS_ROLE + username);
            log.info(">>>>>>>>> 用户{}登出成功 >>>>>>>>>", username);
            new ResponseUtil(response, ResultEnum.LOGOUT_SUCCESS.getCode(), true, ResultEnum.LOGOUT_SUCCESS.getReasonPhraseCN(), null);//登出成功
        }
    }

    //定义登陆失败返回信息
    private class AuthenticationFailureHandlerImpl extends SimpleUrlAuthenticationFailureHandler {
        /*   request：相当与HttpServletRequest
             response：相当与HttpServletRespose
             e：这里保存了我们登录失败的原因
        */
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
            String rm = "";
            // BadCredentialsException 密码错误
            // UsernameNotFoundException 用户名不存咋
            if (e instanceof UsernameNotFoundException) {
                new ResponseUtil(response, ResultEnum.THE_USER_DOES_NOT_EXIST.getCode(),
                        false, ResultEnum.THE_USER_DOES_NOT_EXIST.getReasonPhraseCN(), null);//当前用户已被禁用，请联系管理员处理
            } else if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {
                new ResponseUtil(response, ResultEnum.LOGIN_PASSWORD_ERROR.getCode(),
                        false, ResultEnum.LOGIN_PASSWORD_ERROR.getReasonPhraseCN(), null);//当前用户已被禁用，请联系管理员处理
            } else if (e instanceof LockedException) {
                new ResponseUtil(response, ResultEnum.ACCOUNT_LOCKED.getCode(),
                        false, ResultEnum.ACCOUNT_LOCKED.getReasonPhraseCN(), null);//账户被锁定，请联系管理员
            } else if (e instanceof CredentialsExpiredException) {
                new ResponseUtil(response, ResultEnum.PASSWORD_EXPIRED.getCode(),
                        false, ResultEnum.PASSWORD_EXPIRED.getReasonPhraseCN(), null);//密码过期，请联系管理员
                new ResponseUtil(response, ResultEnum.ACCOUNT_LOCKED.getCode(),
                        false, ResultEnum.ACCOUNT_LOCKED.getReasonPhraseCN(), null);//登录已失效，请重新登陆(账户过期)
            } else if (e instanceof AccountExpiredException) {
                new ResponseUtil(response, ResultEnum.LOGIN_HAS_EXPIRED.getCode(),
                        false, ResultEnum.LOGIN_HAS_EXPIRED.getReasonPhraseCN(), null);//登录已失效，请重新登陆(账户过期)
            } else if (e instanceof DisabledException) {
                new ResponseUtil(response, ResultEnum.USER_HAS_BEEN_DISABLED.getCode(),
                        false, ResultEnum.USER_HAS_BEEN_DISABLED.getReasonPhraseCN(), null);//当前用户已被禁用，请联系管理员处理
            } else {
                new ResponseUtil(response, ResultEnum.LOG_IN_FAILED.getCode(),
                        false, ResultEnum.LOG_IN_FAILED.getReasonPhraseCN(), null);//当前用户已被禁用，请联系管理员处理
            }
        }
    }

    //AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
    public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException {
            // 未登录，无权访问
            new ResponseUtil(response, ResultEnum.NOT_LOGGED_IN.getCode(),
                    false, ResultEnum.NOT_LOGGED_IN.getReasonPhraseCN(), null);//未登录，请重新登录

        }
    }

    //AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
    public class CustomAccessDeineHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
            // 已登录，但无权访问资源
            new ResponseUtil(response, ResultEnum.LOGGED_IN_BUT_NO_PERMISSION.getCode(),
                    false, ResultEnum.LOGGED_IN_BUT_NO_PERMISSION.getReasonPhraseCN(), null);
        }
    }

}
